/*
 * Copyright 2010
 * Jairou Software Organization
 * All Rights Reserved.
 * 
 * This software is the proprietary 
 * information of Roger Ramia
 * and the Jairou Software Organization 
 * Use is subject to license terms.
 * 
 * For licensing information please visit
 * http://www.jairou.org/license
 * 
 * @author Roger Ramia
 */
package org.jairou.util;

import java.io.StringWriter;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;

import org.jairou.core.Objects;

/**
 * Common utility providing core JSON 
 * encoding and decoding support
 * <br/>
 * JSON is a javascript object notation format supported
 * by web browsers and adopted by most modern platforms
 * as a data exchange medium between web browser and server
 * side web applications.   
 * <br/>
 * More information about the JSON specification
 * can be found at www.json.org
 * <br/>
 * This class enables the encoding of Java objects to
 * JSON strings and decoding of JSON strings into
 * Java Objects.  
 * <br/>
 * Much of the decoding logic was ported from an 
 * open source C# implementation found at www.jsonet.com
 * <br/>
 * The primary functions are 
 * <pre>
 * 	JSON.encode() - accepts a java object and returns a JSON string
 * 					or writes to an output stream
 *  JSON.decode() - accepts a JSON string and convert it into a Java object
 * </pre>
 */
public class JSON
{
	private static final int TOKEN_NONE = 0; 
	private static final int TOKEN_CURLY_OPEN = 1;
	private static final int TOKEN_CURLY_CLOSE = 2;
	private static final int TOKEN_SQUARED_OPEN = 3;
	private static final int TOKEN_SQUARED_CLOSE = 4;
	private static final int TOKEN_COLON = 5;
	private static final int TOKEN_COMMA = 6;
	private static final int TOKEN_STRING = 7;
	private static final int TOKEN_NUMBER = 8;
	private static final int TOKEN_TRUE = 9;
	private static final int TOKEN_FALSE = 10;
	private static final int TOKEN_NULL = 11;
	
	private char[] json;
	private int index;	

	public JSON() {}
	
	private JSON(String in) {
		if (in != null) {
			json = in.toCharArray();
			index = 0;
		}
	}
	
	/**
	 * converts all the properties of an Object
	 * to a JSON string per the JSON specification.
	 * Collections and Arrays are converted to JSON arrays []
	 * while Maps and POJOs are converted to JSON objects {}
	 * Native Java Objects are converted to their respective
	 * Javascript equivalent.  
	 * i.e. string to string; numeric to numeric; boolean to boolean
	 * null is converted to null
	 * 
	 * @param obj any java object
	 * @return - JSON formatted string representing the object
	 * @throws Exception - typically reflection exceptions
	 */
	public static String encode(Object obj) 
	throws Exception {
		StringWriter out = new StringWriter();
		encode(obj, out);
		return out.toString();
	}
	
	
	/**
	 * overloaded encode method which converts all the properties 
	 * of an Object to a JSON string per the JSON specification.
	 * Collections and Arrays are converted to JSON arrays []
	 * while Maps and POJOs are converted to JSON objects {}
	 * Native Java Objects are converted to their respective
	 * Javascript equivalent i.e. String to string; numeric to numeric; boolean to boolean
	 * null is converted to null
	 * @param obj - any object to be converted
	 * @param out - writer to write the resulting JSON string
	 * @throws Exception - typically reflection exceptions
	 */
	public static void encode(Object obj, Writer out) 
	throws Exception {
		if (out != null) {
			if (obj == null) out.write("null");
			else {
				if (obj instanceof Collection<?>) {
					out.write('[');
					int count = 0;
					Collection col = ((Collection<Object>) obj);
					for (Object item: col) {
						count++;
						encode(item, out);
						if (count < col.size()) out.write(',');
					}
					out.write(']');
				}
				else if (!Objects.isNative(obj)) {
					out.write('{');
					Collection keys = Objects.keys(obj);
					int count = 0;
					for (Object key: keys) {
						count ++;
						Object val = Objects.get(key.toString(), obj);
						out.write('"');
						out.write(key.toString());
						out.write('"');
						out.write(':');
						if (Objects.isNative(val)) {
							if (Objects.isNumeric(val) || 
								Objects.isBoolean(val)) {
								out.write(val.toString());
							}
							else {
								out.write('"');
								out.write(val.toString());
								out.write('"');	
							}
						}
						else encode(val, out);
						if (count < keys.size()) out.write(',');
					}
					out.write('}');
				}
				else {
					if (Objects.isNumeric(obj) || Objects.isBoolean(obj)) {
						out.write(obj.toString());
					}
					else {
						out.write('"');
						out.write(obj.toString());
						out.write('"');	
					}
				}	
			}
		}
	}
	
	/**
	 * Takes a JSON string and converts into a Java Object.
	 * Objects are parsed into a Map while Arrays are parsed
	 * into a List.  Native types are parse to their respective
	 * Java equivalent i.e. string to string; numeric to numeric; boolean to boolean
	 * null is parsed to null
	 * @param in - JSON encoded string
	 * @return Object - Java object representing the parsed string
	 */
	public static Object decode(String in)
	{
		Object value = null;
		if (in != null) {
			JSON json = new JSON(in);
			value = json.parseValue();
		}		
        return value;
	}

	private Map<String, Object> parseObject()
	{
		Map<String, Object> table = new HashMap<String, Object>();
		int token;

		// {
		nextToken();

		boolean done = false;
		while (!done) {
			token = lookAhead(index);
			if (token == TOKEN_NONE) return null;
			else if (token == TOKEN_COMMA) nextToken();
			else if (token == TOKEN_CURLY_CLOSE) {
				nextToken();
				return table;
			} 
			else {
				// parse the name
				String name = parseString();
				if (name == null) return null;
				// skip over the :
				token = nextToken();
				if (token != TOKEN_COLON) return null;
				table.put(name, parseValue());
			}
		}

		return table;
	}

	private Collection<Object> parseArray()
	{
		Collection<Object> array = new ArrayList<Object>();
		// skip the [ and start array
		nextToken();
		boolean done = false;
		while (!done) {
			int token = lookAhead(index);
			if (token == TOKEN_NONE) return null;
			else if (token == TOKEN_COMMA)nextToken();
			else if (token == TOKEN_SQUARED_CLOSE) {
				// skip the ] and end array
				nextToken();
				break;
			} 
			else array.add(parseValue());
		}
		return array;
	}

	private Object parseValue()
	{
		switch (lookAhead(index)) {
			case TOKEN_STRING:
				return parseString();
			case TOKEN_NUMBER:
				return parseNumber();
			case TOKEN_CURLY_OPEN:
				return parseObject();
			case TOKEN_SQUARED_OPEN:
				return parseArray();
			case TOKEN_TRUE:
				nextToken();
				return true;
			case TOKEN_FALSE:
				nextToken();
				return false;
			case TOKEN_NULL:
				nextToken();
				return null;
			case TOKEN_NONE:
				break;
		}
		return null;
	}

	private String parseString()
	{
		String s = "";
		char c;
		eatWhitespace();		
		// "
		c = json[index++];
		if (c != '"') index--;
		boolean complete = false;
		while (!complete) {
			if (index == json.length) break;
			c = json[index++];
			if (c == '"' || c == ':') {
				complete = true;
				if (c == ':') index--;
				break;
			} 
			else if (c == '\\') {
				if (index == json.length) break;
				c = json[index++];
				if (c == '"') s += '"';
				else if (c == '\\') s += '\\';
				else if (c == '/') s += '/';
				else if (c == 'b') s += '\b';
				else if (c == 'f') s += '\f';
				else if (c == 'n') s += '\n';
				else if (c == 'r') s += '\r';
				else if (c == 't') s += '\t';
				else if (c == 'u') {
					int remainingLength = json.length - index;
					if (remainingLength >= 4) {
						s += (char)Integer.parseInt(new String(Arrays.copyOfRange(json, index, (index+4))), 16);
						// skip 4 chars
						index += 4;
					} else {
						break;
					}
				}

			} 
			else s += c;
		}
		if (!complete) return null;
		return s;
	}

	private double parseNumber()
	{
		eatWhitespace();

		int lastIndex = getLastIndexOfNumber(index);
		char[] numberCharArray = Arrays.copyOfRange(json, index, lastIndex+1);
		index = lastIndex + 1;
		return Double.parseDouble(new String(numberCharArray));
	}

	private int getLastIndexOfNumber(int index)
	{
		int lastIndex;
		for (lastIndex = index; lastIndex < json.length; lastIndex++) {
			if ("0123456789+-.eE".indexOf(json[lastIndex]) == -1) {
				break;
			}
		}
		return lastIndex - 1;
	}

	private void eatWhitespace()
	{
		while (index < json.length) {
			if (!Character.isWhitespace(json[index])){
				break;
			}
			index++;
		}
	}

	private int lookAhead(int indexCopy)
	{
		eatWhitespace();
		
		indexCopy = index;
		if (indexCopy == json.length) {
			return TOKEN_NONE;
		}
		
		char c = json[indexCopy];
		indexCopy++;
		switch (c) {
			case '{':
				return TOKEN_CURLY_OPEN;
			case '}':
				return TOKEN_CURLY_CLOSE;
			case '[':
				return TOKEN_SQUARED_OPEN;
			case ']':
				return TOKEN_SQUARED_CLOSE;
			case ',':
				return TOKEN_COMMA;
			case '"':
				return TOKEN_STRING;
			case '0': case '1': case '2': case '3': case '4': 
			case '5': case '6': case '7': case '8': case '9':
			case '-': 
				return TOKEN_NUMBER;
			case ':':
				return TOKEN_COLON;
		}
		indexCopy--;

		int remainingLength = json.length - indexCopy;

		// false
		if (remainingLength >= 5) {
			if (json[indexCopy] == 'f' &&
				json[indexCopy + 1] == 'a' &&
				json[indexCopy + 2] == 'l' &&
				json[indexCopy + 3] == 's' &&
				json[indexCopy + 4] == 'e') {
				indexCopy += 5;
				return TOKEN_FALSE;
			}
		}

		// true
		if (remainingLength >= 4) {
			if (json[indexCopy] == 't' &&
				json[indexCopy + 1] == 'r' &&
				json[indexCopy + 2] == 'u' &&
				json[indexCopy + 3] == 'e') {
				indexCopy += 4;
				return TOKEN_TRUE;
			}
		}

		// null
		if (remainingLength >= 4) {
			if (json[indexCopy] == 'n' &&
				json[indexCopy + 1] == 'u' &&
				json[indexCopy + 2] == 'l' &&
				json[indexCopy + 3] == 'l') {
				indexCopy += 4;
				return TOKEN_NULL;
			}
		}
		
		// catch unquoted string property names
		if (remainingLength > 3) {
			return TOKEN_STRING;
		}

		return TOKEN_NONE;
	}

	private int nextToken()
	{
		eatWhitespace();

		if (index == json.length) {
			return TOKEN_NONE;
		}
		
		char c = json[index];
		index++;
		switch (c) {
			case '{':
				return TOKEN_CURLY_OPEN;
			case '}':
				return TOKEN_CURLY_CLOSE;
			case '[':
				return TOKEN_SQUARED_OPEN;
			case ']':
				return TOKEN_SQUARED_CLOSE;
			case ',':
				return TOKEN_COMMA;
			case '"':
				return TOKEN_STRING;
			case '0': case '1': case '2': case '3': case '4': 
			case '5': case '6': case '7': case '8': case '9':
			case '-': 
				return TOKEN_NUMBER;
			case ':':
				return TOKEN_COLON;
		}
		index--;

		int remainingLength = json.length - index;

		// false
		if (remainingLength >= 5) {
			if (json[index] == 'f' &&
				json[index + 1] == 'a' &&
				json[index + 2] == 'l' &&
				json[index + 3] == 's' &&
				json[index + 4] == 'e') {
				index += 5;
				return TOKEN_FALSE;
			}
		}

		// true
		if (remainingLength >= 4) {
			if (json[index] == 't' &&
				json[index + 1] == 'r' &&
				json[index + 2] == 'u' &&
				json[index + 3] == 'e') {
				index += 4;
				return TOKEN_TRUE;
			}
		}

		// null
		if (remainingLength >= 4) {
			if (json[index] == 'n' &&
				json[index + 1] == 'u' &&
				json[index + 2] == 'l' &&
				json[index + 3] == 'l') {
				index += 4;
				return TOKEN_NULL;
			}
		}
		
		// catch unquoted string property names
		if (remainingLength > 3) {
			return TOKEN_STRING;
		}

		return TOKEN_NONE;
	}
}


